package com.noahsloan.nutils.collections;

import java.util.AbstractSet;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import com.noahsloan.nutils.Equals;
import com.noahsloan.nutils.Hasher;

/**
 * Wrapper around a {@link HashMap} that allows an external hash code and equals
 * method for the keys. i.e. you get to decide what makes two keys equal, not
 * the author of the key class.
 * 
 * @author noah
 * 
 * @param <K>
 * @param <V>
 */
public class ExternalHashMap<K, V> extends ExternalHashBase<K> implements
		Map<K, V> {

	private static final long serialVersionUID = 1L;

	protected final Map<ExternalHashBase<K>.HashWrapper, V> map;

	protected ExternalHashMap(Map<HashWrapper, V> map, Hasher<? super K> hash,
			Equals<? super K, ? super K> equals) {
		super(hash, equals);
		this.map = map;
	}

	public ExternalHashMap(Hasher<? super K> hash,
			Equals<? super K, ? super K> equals) {
		this(new HashMap<ExternalHashBase<K>.HashWrapper, V>(), hash, equals);
	}

	public ExternalHashMap(Hasher<? super K> hash,
			Equals<? super K, ? super K> equals, int initialCapacity,
			float loadFactor) {
		this(new HashMap<ExternalHashBase<K>.HashWrapper, V>(initialCapacity,
				loadFactor), hash, equals);
	}

	public ExternalHashMap(Hasher<? super K> hash,
			Equals<? super K, ? super K> equals, int initialCapacity) {
		this(new HashMap<ExternalHashBase<K>.HashWrapper, V>(initialCapacity),
				hash, equals);
	}

	public ExternalHashMap(Hasher<? super K> hash,
			Equals<? super K, ? super K> equals, Map<? extends K, ? extends V> m) {
		this(hash, equals);
		putAll(m);
	}

	public void clear() {
		map.clear();
	}

	@SuppressWarnings("unchecked")
	public boolean containsKey(Object key) {
		return map.containsKey(new HashWrapper((K) key));
	}

	/**
	 * THIS METHOD DOES NOT USE AN EXTERNAL EQUALS. It will use the normal
	 * object identity. If you don't like that, create a subclass that also
	 * takes an {@link Equals} for the value.
	 */
	public boolean containsValue(Object value) {
		return map.containsValue(value);
	}

	public Set<java.util.Map.Entry<K, V>> entrySet() {
		return new AbstractSet<Entry<K, V>>() {

			@Override
			public Iterator<java.util.Map.Entry<K, V>> iterator() {
				return new Iterator<Entry<K, V>>() {

					private Iterator<Entry<HashWrapper, V>> it;

					public boolean hasNext() {
						return it.hasNext();
					}

					public java.util.Map.Entry<K, V> next() {
						final Entry<HashWrapper, V> next = it.next();
						return new Entry<K, V>() {
							public K getKey() {
								return next.getKey().value;
							}

							public V getValue() {
								return next.getValue();
							}

							public V setValue(V value) {
								return next.setValue(value);
							}
						};
					}

					public void remove() {
						it.remove();
					}
				};
			}

			@Override
			public int size() {
				return ExternalHashMap.this.size();
			}

		};
	}

	@SuppressWarnings("unchecked")
	public V get(Object key) {
		return map.get(new HashWrapper((K) key));
	}

	public boolean isEmpty() {
		return map.isEmpty();
	}

	public Set<K> keySet() {
		return new AbstractSet<K>() {
			@Override
			public Iterator<K> iterator() {
				final Iterator<HashWrapper> it = map.keySet().iterator();
				return new Iterator<K>() {

					public boolean hasNext() {
						return it.hasNext();
					}

					public K next() {
						return it.next().value;
					}

					public void remove() {
						it.remove();
					}

				};
			}

			@Override
			public int size() {
				return map.keySet().size();
			}

		};
	}

	public V put(K key, V value) {
		return map.put(new HashWrapper(key), value);
	}

	@SuppressWarnings("unchecked")
	public void putAll(Map<? extends K, ? extends V> t) {
		Set<Entry<? extends K, ? extends V>> entrySet = (Set) t.entrySet();
		for (Entry<? extends K, ? extends V> entry : entrySet) {
			put(entry.getKey(), entry.getValue());
		}
	}

	@SuppressWarnings("unchecked")
	public V remove(Object key) {
		return map.remove(new HashWrapper((K) key));
	}

	public int size() {
		return map.size();
	}

	public Collection<V> values() {
		return map.values();
	}

}
